/**
 * @class UI动画切换
 * @author DuskyHuang 圣子
 * @description 提供常见的渐入渐出动画和灵活参数调整，使得UI快捷生动。
*/

import { _decorator, TweenEasing, Enum, Node, Tween, UIOpacity, tween, Component, Vec3, Widget, UITransform, Button, NodeEventType, Event } from 'cc';
import { Direction8, ExecutionOrder } from '../../configs';
import { group, tip } from '../../utils';
const { ccclass, requireComponent, executionOrder } = _decorator;

export enum EffectType { NONE, FADE, SLIDE, FADE_SLIDE, SCALE, FADE_SCALE, ROTATE_SCALE, FADE_ROTATE, FADE_ROTATE_SCALE }
export enum EasingType { LINEAR, SMOOTH, FADE, CONSTANT, QUAD, CUBIC, QUART, QUINT, SINE, CIRC, EXPO, ELASTIC, BACK, BOUNCE }
export enum EasingTendency { OUT, IN, IN_OUT, OUT_IN }
const NoTendency = [EasingType.LINEAR, EasingType.SMOOTH, EasingType.FADE, EasingType.CONSTANT];

function useShowTween(this: TweenSwitcher) { return this.showType != EffectType.NONE; }
function showTendency(this: TweenSwitcher) { return this.showType != EffectType.NONE && NoTendency.excludes(this.showEasing); }
function useShowSlide(this: TweenSwitcher) { return this.showType === EffectType.FADE_SLIDE || this.showType === EffectType.SLIDE; }
function useShowScale(this: TweenSwitcher) { return this.showType === EffectType.FADE_SCALE || this.showType === EffectType.SCALE; }
function useShowRotate(this: TweenSwitcher) { return this.showType === EffectType.ROTATE_SCALE || this.showType === EffectType.FADE_ROTATE || this.showType === EffectType.FADE_ROTATE_SCALE; }

function useHideTween(this: TweenSwitcher) { return this.hideType != EffectType.NONE; }
function hideTendency(this: TweenSwitcher) { return this.hideType != EffectType.NONE && NoTendency.excludes(this.hideEasing); }
function useHideSlide(this: TweenSwitcher) { return this.hideType === EffectType.FADE_SLIDE || this.hideType === EffectType.SLIDE; }
function useHideScale(this: TweenSwitcher) { return this.hideType === EffectType.FADE_SCALE || this.hideType === EffectType.SCALE; }
function useHideRotate(this: TweenSwitcher) { return this.hideType === EffectType.ROTATE_SCALE || this.hideType === EffectType.FADE_ROTATE || this.hideType === EffectType.FADE_ROTATE_SCALE; }

class TweenStates {
    constructor(
        public position: Vec3 = new Vec3,
        public scale: Vec3 = new Vec3,
        public opacity: number = 0,
        public angle: num.deg = 0
    ) { this.scale.z = 1; }
    public reset(other: TweenStates): this;
    public reset(position: Vec3, scale: Vec3, opacity?: number, angle?: number): this;
    public reset(arg1: TweenStates | Vec3, arg2?: Vec3, arg3: number = 0, arg4: number = 0): this {
        if (arg1 instanceof TweenStates) {
            this.position.set(arg1.position);
            this.scale.set(arg1.scale);
            this.opacity = arg1.opacity;
            this.angle = arg1.angle;
        } else {
            this.position.set(arg1);
            this.scale.set(arg2);
            this.opacity = arg3;
            this.angle = arg4;
        }
        return this;
    }
}

@ccclass('cfw.TweenSwitcher')
@requireComponent(UITransform)
@executionOrder(ExecutionOrder.AFTER_WIDGET)
export class TweenSwitcher extends Component {

    public static readonly EffectType: typeof EffectType = EffectType;
    public static readonly EasingType: typeof EasingType = EasingType;
    public static readonly Direction: typeof Direction8 = Direction8;
    public get showing(): boolean { return this._showing; }

    @tip("缓动中不交互")
    public interactableInTween: boolean = false;

    @group("Show", "动效类型", "Type", ["type", Enum(EffectType)])
    public showType: EffectType = EffectType.NONE;
    @group("Show", "缓动方式", useShowTween, "Easing", ["type", Enum(EasingType)])
    public showEasing: EasingType = EasingType.SINE;
    @group("Show", "动画时长", useShowTween, "Duration", [0, 10, 0.1], ["unit", "sec"])
    public showDuration: time.sec = 0.3;
    @group("Show", "渐入方式", showTendency, "Tendency", ["type", Enum(EasingTendency)])
    public showTendency: EasingTendency = EasingTendency.OUT;
    @group("Show", "渐入方向", useShowSlide, "Direction", ["type", Enum(Direction8)])
    public showDirection: Direction8 = Direction8.UP;
    @group("Show", "渐入距离", useShowSlide, "Distance", [0, 1000, 1], ["unit", "px"])
    public showDistance: num.px = 200;
    @group("Show", "渐入延迟", useShowTween, "Delay", [0, 10, 0.1], ["unit", "sec"])
    public showDelay: time.sec = 0;
    @group("Show", "起始比例", useShowScale, "FromScale", [0, 10, 0.1])
    public showFromScale: num.ratio = 0;
    @group("Show", "旋转角度", useShowRotate, "Angle", [0, 720, 15], ["unit", "deg"])
    public showRotateAngle: num.deg = 360;
    @group("Show", "播前就位", useShowTween, "PreReady")
    public showPreReady: boolean = false;

    @group("Hide", "动效类型", "Type", ["type", Enum(EffectType)])
    public hideType: EffectType = EffectType.NONE;
    @group("Hide", "缓动方式", useHideTween, "Easing", ["type", Enum(EasingType)])
    public hideEasing: EasingType = EasingType.SINE;
    @group("Hide", "动画时长", useHideTween, "Duration", [0, 10, 0.1], ["unit", "sec"])
    public hideDuration: time.sec = 0.3;
    @group("Hide", "渐出方式", hideTendency, "Tendency", ["type", Enum(EasingTendency)])
    public hideTendency: EasingTendency = EasingTendency.OUT;
    @group("Hide", "渐出方向", useHideSlide, "Direction", ["type", Enum(Direction8)])
    public hideDirection: Direction8 = Direction8.DOWN;
    @group("Hide", "渐出距离", useHideSlide, "Distance", [0, 1000, 1], ["unit", "px"])
    public hideDistance: num.px = 200;
    @group("Hide", "渐出延迟", useHideTween, "Delay", [0, 10, 0.1], ["unit", "sec"])
    public hideDelay: time.sec = 0;
    @group("Hide", "结束比例", useHideScale, "ToScale", [0, 10, 0.1])
    public hideToScale: num.ratio = 0;
    @group("Hide", "旋转角度", useHideRotate, "Angle", [0, 720, 15], ["unit", "deg"])
    public hideRotateAngle: num.deg = 360;
    @group("Hide", "播完隐藏", useHideTween, "PostRemove")
    public hidePostInactive: boolean = false;

    private get _opacity(): UIOpacity { return this.getComponent(UIOpacity) ?? this.addComponent(UIOpacity); }
    private _origin: TweenStates = new TweenStates;
    private _from: TweenStates = new TweenStates;
    private _to: TweenStates = new TweenStates;
    private _showing: boolean = true;

    // 播放出现或隐藏
    public play(toShow: boolean, withActivate?: true): this { toShow ? this.show(withActivate) : this.hide(withActivate); return this; }

    // 状态切换
    public toggle(): this { this._showing ? this.hide() : this.show(); return this; }

    // 播放出现
    public show(activeImmediately?: true): this { if (activeImmediately) this.node.active = true; this._showing = true; this._startTween(); return this; }

    // 播放隐藏
    public hide(inactiveWhenCompleted?: true): this { if (inactiveWhenCompleted) this.hidePostInactive = true; this._showing = false; this._startTween(); return this; }

    protected onLoad(): void {
        const widget = this.getComponent(Widget);
        if (widget && widget.enabled) widget.updateAlignment();
        this.resetOrigin();
        if (this.showPreReady && this.showType != EffectType.NONE) this.setReady();
    }

    public resetOrigin(): this {
        this._origin.reset(
            this.node.position,
            this.node.scale,
            this.getComponent(UIOpacity)?.opacity ?? 255,
            this.node.angle
        );
        return this;
    }

    public resetOriginPosition(): this {
        this._origin.position.set(this.node.position);
        return this;
    }

    public setReady(): this {
        this._reCalculate();
        this._setStatesAs(this._from);
        return this;
    }

    private _reCalculate(): void {
        const type = this._showing ? this.showType : this.hideType;
        this._from.reset(this._origin);
        this._to.reset(this._origin);
        switch (type) {
            case EffectType.FADE: this._fade(); break;
            case EffectType.SLIDE: this._slide(); break;
            case EffectType.SCALE: this._scale(); break;
            case EffectType.FADE_SLIDE: this._fade()._slide(); break;
            case EffectType.FADE_SCALE: this._fade()._scale(); break;
            case EffectType.FADE_ROTATE: this._fade()._rotate(); break;
            case EffectType.ROTATE_SCALE: this._rotate()._scale(); break;
            case EffectType.FADE_ROTATE_SCALE: this._fade()._rotate()._scale(); break;
        }
    }

    private _startTween(): void {
        this._reCalculate();
        if (this._showing && this.showType === EffectType.NONE) {
            this._setStatesAs(this._origin); return;
        }
        if (!this._showing && this.hideType === EffectType.NONE) {
            this._setStatesAs(this._origin); return;
        }
        Tween.stopAllByTarget(this);
        const easType = this._showing ? this.showEasing : this.hideEasing;
        const tend = this._showing ? this.showTendency : this.hideTendency;
        const delay = this._showing ? this.showDelay : this.hideDelay;
        const duration = this._showing ? this.showDuration : this.hideDuration;
        this._setStatesAs(this._from);
        tween(this.node)
            .delay(delay)
            .to(duration, null, {
                easing: this._parseEasing(easType, tend),
                onStart: () => this.interactableInTween && this._setInteractable(false),
                onUpdate: (self: Node, ratio: number) => {
                    self.setPosition(Vec3.lerp(v3a, this._from.position, this._to.position, ratio));
                    self.setScale(Vec3.lerp(v3a, this._from.scale, this._to.scale, ratio));
                    const opa = this.getComponent(UIOpacity);
                    if (opa) opa.opacity = Math.lerp(this._from.opacity, this._to.opacity, ratio);
                    self.angle = Math.lerp(this._from.angle, this._to.angle, ratio);
                },
                onComplete: (self: Node) => {
                    this.interactableInTween && this._setInteractable(true);
                    if (!this._showing && this.hidePostInactive) {
                        self.active = false;
                    }
                }
            })
            .start();
    }

    // 淡入淡出
    private _fade(): this {
        const opa = this._origin.opacity;
        this._to.opacity = this._showing ? opa : 0;
        this._from.opacity = this._showing ? 0 : opa;
        return this;
    }

    // 滑入滑出
    private _slide(): this {
        const widget = this.getComponent(Widget);
        if (widget && widget.enabled && widget.alignMode === Widget.AlignMode.ALWAYS) {
            console.log(this.node.name)
            console.warn("[TweenShift]: 'SLIDE' may not useful course of widget (MODE=ALWAYS)");
        }
        const dir = this._showing ? this.showDirection : this.hideDirection;
        const dist = this._showing ? this.showDistance : this.hideDistance;
        const Dir = Direction8;
        const r = [Dir.RIGHT, Dir.UP_RIGHT, Dir.DOWN_RIGHT].contains(dir);
        const l = [Dir.LEFT, Dir.UP_LEFT, Dir.DOWN_LEFT].contains(dir);
        const u = [Dir.UP, Dir.UP_LEFT, Dir.UP_RIGHT].contains(dir);
        const d = [Dir.DOWN, Dir.DOWN_LEFT, Dir.DOWN_RIGHT].contains(dir);
        const { x, y } = this._origin.position;
        this._from.position.set(x, y);
        this._to.position.set(x, y);
        if (this._showing) {
            this._from.position.x += r ? - dist : l ? dist : 0;
            this._from.position.y += u ? - dist : d ? dist : 0;
        } else {
            this._to.position.x += r ? dist : l ? -dist : 0;
            this._to.position.y += u ? dist : d ? -dist : 0;
        }
        return this;
    }

    // 旋入旋出
    private _rotate(): this {
        const add = this._showing ? this.showRotateAngle : this.hideRotateAngle;
        const angle = this._origin.angle;
        this._to.angle = this._showing ? angle : angle + add;
        this._from.angle = this._showing ? angle + add : angle;
        return this;
    }

    // 缩放出入
    private _scale(): this {
        const { x, y } = this._origin.scale;
        // NOTICE! scale.z = 0 will lost 2D touchable events!
        this._from.scale.set(this._showing ? x * this.showFromScale : x, this._showing ? y * this.showFromScale : y, 1);
        this._to.scale.set(this._showing ? x : x * this.hideToScale, this._showing ? y : y * this.hideToScale, 1);
        return this;
    }

    private _setStatesAs(states: TweenStates): void {
        const { position, scale, angle, opacity } = states;
        this.node.setPosition(position);
        this.node.setScale(scale);
        this.node.angle = angle;
        if (isNum(opacity)) this._opacity.opacity = opacity;
    }

    private _parseEasing(easing: EasingType, tendency: EasingTendency): TweenEasing {
        if (easing === EasingType.LINEAR) return 'linear';
        if (easing === EasingType.SMOOTH) return 'smooth';
        if (easing === EasingType.FADE) return 'fade';
        if (easing === EasingType.CONSTANT) return 'constant';
        let prefix: string, suffix: string;
        switch (easing) {
            case EasingType.QUAD: prefix = 'quad'; break;
            case EasingType.CUBIC: prefix = 'cubic'; break;
            case EasingType.QUART: prefix = 'quart'; break;
            case EasingType.QUINT: prefix = 'quint'; break;
            case EasingType.SINE: prefix = 'sine'; break;
            case EasingType.CIRC: prefix = 'circ'; break;
            case EasingType.EXPO: prefix = 'expo'; break;
            case EasingType.ELASTIC: prefix = 'elastic'; break;
            case EasingType.BACK: prefix = 'back'; break;
            case EasingType.BOUNCE: prefix = 'bounce'; break;
        }
        switch (tendency) {
            case EasingTendency.IN: suffix = 'In'; break;
            case EasingTendency.OUT: suffix = 'Out'; break;
            case EasingTendency.IN_OUT: suffix = 'InOut'; break;
            case EasingTendency.OUT_IN: suffix = 'OutIn'; break;
        }
        return prefix + suffix as TweenEasing;
    }

    private _setInteractable(arg: boolean): void {
        for (let i = 0; i < BlockEvents.length; i++) {
            arg ? this.node.on(BlockEvents[i], stopPropagation, this) :
                this.node.off(BlockEvents[i], stopPropagation, this);
        };
    }
}

const BlockEvents = [NodeEventType.TOUCH_START, NodeEventType.TOUCH_END, NodeEventType.TOUCH_MOVE,
NodeEventType.MOUSE_DOWN, NodeEventType.MOUSE_MOVE, NodeEventType.MOUSE_UP,
NodeEventType.MOUSE_ENTER, NodeEventType.MOUSE_LEAVE, NodeEventType.MOUSE_WHEEL];

function stopPropagation(event: Event) {
    event.propagationStopped = true;
}

const v3a = new Vec3;